Fix kernel async statement telemetry handle - #923
Conversation
410999a to
7deecf2
Compare
7deecf2 to
6629d87
Compare
There was a problem hiding this comment.
Verdict: 1 Low
Looks good — the owning-handle-first / attach-by-id-fallback change is correct: the claim is check-and-set under _async_handles_lock, cleanup is consistent across close_command/close_session, and the new unit + e2e tests cover the main paths. One low-severity note on an unclear telemetry-finalization edge when result-set construction fails after a successful await_result().
| stream = handle.await_result() | ||
| except Exception as exc: | ||
| if uses_owning_handle: | ||
| with self._async_handles_lock: |
There was a problem hiding this comment.
🔵 Low — The owning-handle failure path only discards _async_result_stream_started when await_result() raises. If await_result() succeeds (marker stays set) but the subsequent KernelResultSet.__init__ → arrow_schema() raises and is re-wrapped, the guid remains marked as started. A later retry then takes the attach-by-id (no-op telemetry) branch.
Whether this loses the ExecuteStatementAsync telemetry row depends on when the kernel finalizes it: if finalization happens when await_result() returns, this is harmless (telemetry already committed). If finalization only completes once the result stream is drained, the telemetry is lost on this retry because the owning handle is never reused. The PR's own comments ("first in-process result stream", "clear the claimed marker so a retry can still use the telemetry-bearing owning handle") are ambiguous on this point, and the added test_get_execution_result_owning_handle_failure_can_retry_owning_handle only exercises the await_result()-raises case, not the construct-failure-after-await case. Worth confirming the finalization semantics and, if drain-based, discarding the marker on the construction-failure path too.
There was a problem hiding this comment.
⛔ NEEDS HUMAN DECISION — the bots can't resolve this thread; a maintainer's input is required.
No safe code change is actionable here, and the deciding fact needs kernel-team judgment. The reviewer's suggested fix — discard _async_result_stream_started on the construction-failure path (await succeeds, then _make_result_set/arrow_schema() raises) — would be unsafe: the owning handle's await_result() is single-shot by design (confirmed by test_get_execution_result_is_re_callable, which deliberately re-attaches by id for the second fetch instead of re-calling owning_handle.await_result()). Discarding the marker would route a retry back through the owning handle and re-invoke await_result() on an already-consumed stream — the exact double-consume the marker prevents. Moreover, once await_result() has returned, the stream is materialized and single-shot, so attach-by-id is the only correct retry path regardless. Whether the rare construct-failure-after-await case actually loses the ExecuteStatementAsync telemetry row depends on kernel-internal finalization timing (finalize-at-return vs finalize-at-drain), which has no observable surface in databricks-sql-python and cannot be verified from this repo or a unit test here. The module's own comments assume return-based finalization (making the concern moot); if drain-based, there is no safe in-process remedy. Flagging for a human to confirm finalization semantics with the kernel team.
There was a problem hiding this comment.
Verdict: 1 Low
Solid, well-tested fix. The owning-handle-first / attach-by-id-fallback logic is correctly lock-guarded, the claimed marker is reset on await failure to preserve retry-with-owning-handle, and unit + e2e coverage and lifecycle comments are updated to match. One low-severity edge: the result-set-construction failure path (second except) doesn't reset _async_result_stream_started, so a retry after that failure loses the telemetry-bearing owning handle — see inline note. The is_telemetry_enabled short-circuit for use_kernel (overriding even force_enable_telemetry) is intentional and tested.
There was a problem hiding this comment.
Verdict: 1 Low
Looks good — the async owning-handle ownership model (owning handle for first in-process status/result, attach-by-id for re-fetch/cross-process, marker cleared on close/session-close) is coherent and well covered by the new unit tests, and the use_kernel telemetry short-circuit is intentional (verified against the added test). One Low note about asymmetric marker cleanup when result-set construction fails after a successful await_result().
There was a problem hiding this comment.
Verdict: 1 Medium
Looks solid overall — the owning-handle-first / attach-by-id fallback logic, locking, and lifecycle cleanup (close_command / close_session) are consistent and well-tested. One medium concern: the result-set-construction failure path in get_execution_result doesn't clear the _async_result_stream_started marker the way the sibling await_result failure path does, so a retry after a construction error would silently drop to attach-by-id and lose the async telemetry this PR is meant to preserve.
There was a problem hiding this comment.
Verdict: 1 Low
Looks good — the owning-handle-first / attach-by-id-fallback logic is correct, lock usage is sound, and the marker lifecycle (claim on fetch, discard on await failure, retain on post-await construction failure, clear on close) is consistent across get_query_state, get_execution_result, close_command, and close_session. One low-severity coverage gap: the deliberately-documented "marker retained when result-set construction fails after a successful await" branch isn't locked by a unit test, unlike its sibling await-failure branch.
Addresses: - #3836884412 at src/databricks/sql/backend/kernel/client.py:816 Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Verdict: 1 Low
Solid, well-tested fix — the owning-handle-first / attach-by-id-fallback logic and the _async_result_stream_started marker lifecycle (add on claim, discard on await_result() failure to allow an owning-handle retry, deliberately retain on result-set construction failure to route retries through a fresh attach-by-id stream) are internally consistent and match the added unit tests. One low-severity note inline about the new concurrency surface. Separately (not inline, since it's outside the diff): test_async_result_resumable_from_a_fresh_cursor's docstring claims it proves "state/results come from the server (attach-by-id), not connector-side per-cursor tracking," but because _async_handles is shared per-connection, that same-connection resume now goes through the retained owning handle rather than attach-by-id — the test still passes but no longer exercises the path its docstring describes; consider updating it or resuming from a second connection.
| # concurrent polling of one async id is unsupported — the supported cross-cursor | ||
| # resume path re-attaches by id (the attach-by-id fallback below) once result | ||
| # streaming has been claimed. | ||
| with self._async_handles_lock: |
There was a problem hiding this comment.
🔵 Low — This change trades the previously concurrency-safe behavior (always attach-by-id, where every get_query_state / get_execution_result call created its own server-side handle) for use of a shared owning handle before result streaming is claimed. As the new concurrency note candidly documents, handle.status() (and handle.await_result()) now run outside the lock on the connection-shared owning handle. Note that _async_handles is keyed per-connection, not per-cursor, so a fresh cursor on the same connection that adopts the command id (the resume flow exercised by tests/e2e/test_kernel_backend.py::test_async_result_resumable_from_a_fresh_cursor) will also resolve the owning handle and can race the original submitter's polling on the same underlying kernel handle — not just a hand-rolled two-cursor poll. This is an intentional, documented limitation and the common single-submitter flow is safe, so flagging as low: worth confirming the kernel ExecutedAsyncStatement either tolerates concurrent status() or that same-connection concurrent resume is genuinely out of scope.
There was a problem hiding this comment.
I corrected the misleading documentation the reviewer flagged. The docstring for get_query_state now accurately reflects that the owning-handle path is per-connection — any cursor on the submitting connection (including a fresh cursor resuming the id) resolves the same owning handle until result streaming is claimed — consistent with the inline concurrency note. Tests pass (125 in the kernel client file).
Pushed 23b4330.
There was a problem hiding this comment.
The pushed commit only rewords the docstring — the owning-handle-under-lock behavior at line 721 is unchanged, and the substantive question I raised (does the kernel ExecutedAsyncStatement actually tolerate concurrent status(), or is same-connection concurrent resume genuinely out of scope?) is neither demonstrated nor answered by clearer prose. Better documentation of a limitation isn't verification that the limitation is safe, so I'm keeping this open at Low for a human to confirm the underlying kernel-handle behavior; no further action is needed if that concurrency is confirmed acceptable.
There was a problem hiding this comment.
⛔ NEEDS HUMAN DECISION — the bots can't resolve this thread; a maintainer's input is required.
Converged to a needs-human question that no code change here can settle: whether the kernel wheel's ExecutedAsyncStatement.status() is safe under concurrent calls on the shared per-connection owning handle. A human must confirm one of: (a) the kernel handle's status()/await_result() tolerate concurrent invocation, or (b) same-connection concurrent resume (tests/e2e/test_kernel_backend.py::test_async_result_resumable_from_a_fresh_cursor racing the original submitter's polling) is genuinely out of scope. Verifying this requires reading the databricks-sql-kernel wheel internals or running the e2e resume test against a live warehouse — this follow-up job has no live-warehouse credentials, so it can't run here. Rewording the docstring again was already rejected by the reviewer as non-verification, and dropping the shared owning handle would revert this PR's intended async-telemetry-finalization fix, so neither is an appropriate change. Flagging for human confirmation of kernel-handle concurrency behavior.
Addresses: - #3836901963 at src/databricks/sql/backend/kernel/client.py:716 Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Verdict: 1 Low
Looks good — the owning-handle-first async telemetry fix is carefully implemented and well-covered by unit tests (claim/discard bookkeeping is consistent, and the set is cleaned up in close_command/close_session). One low-severity note: the change shares a single kernel handle for pre-stream status polling and calls status() outside the lock, narrowing a previously-safe concurrent-polling path to "documented-but-unenforced."
Addresses: - #3836923314 at src/databricks/sql/backend/kernel/client.py:729 Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Verdict: 1 Medium
Looks solid overall — the per-method reservation/claim bookkeeping is correct and well-tested. One medium concurrency concern: get_execution_result doesn't consult _async_status_in_flight, so a concurrent status-poll and result-fetch can still operate on the same shared owning handle — the exact cross-thread use the PR's own comment declares unsafe. Narrow in the normal serial poll-then-fetch flow, but worth guarding or documenting.
Addresses: - #3836943236 at src/databricks/sql/backend/kernel/client.py:823 Signed-off-by: peco-engineer-bot[bot] <peco-engineer-bot[bot]@users.noreply.github.com>
Summary
get_query_stateand the first result fetch viaget_execution_result.Testing
.venv/bin/python -m pytest tests/unit/test_kernel_client.py -q